﻿using System;
using System.Collections.Generic;
using System.Linq;
using ch09.binary.module;
using ch09.binary.type;

namespace ch09.binary.interpreter
{
    public struct Vm
    {
        internal OperandStack OperandStack;
        internal ControlStack ControlStack;
        internal Module Module;
        internal Memory Memory;
        public GlobalVar[] Globals;
        public uint Local0Idx;
        public VmFunc[] Funcs;
        public Table Table;

        public Vm(Module mo) : this()
        {
            OperandStack = new OperandStack {Slots = Array.Empty<ulong>()};
            ControlStack = new ControlStack(Array.Empty<ControlFrame>());
            Module = mo;
            Globals ??= Array.Empty<GlobalVar>();
            Funcs ??= Array.Empty<VmFunc>();
        }

        internal void Loop()
        {
            var depth = ControlStack.ControlDepth();
            // var cf = ControlStack[ControlStack.controlDepth() - 1];
            for (; ControlStack.ControlDepth() >= depth;)
            {
                if (ControlStack.Frames[^1].Pc == (ControlStack.Frames[^1].Instrs?.Length ?? 0))
                {
                    ExitBlock();
                }
                else
                {
                    var instr = ControlStack.Frames[^1].Instrs[ControlStack.Frames[^1].Pc];
                    ControlStack.Frames[^1].Pc++;
                    ExecInstr(instr);
                }
            }
        }

        public void InitMem()
        {
            if (Module.MemSec?.Length > 0)
            {
                Memory = new Memory(Module.MemSec[0]);
            }

            if (Module.DataSec == null)
            {
                return;
            }

            foreach (var data in Module.DataSec)
            {
                foreach (var instr in data.Offset)
                {
                    ExecInstr(instr);
                }

                Memory.Write(OperandStack.PopU64(), data.Init);
            }
        }

        public void InitGlobals()
        {
            if (Module.GlobalSec == null)
            {
                return;
            }

            foreach (var global in Module.GlobalSec)
            {
                foreach (var instr in global.Init)
                {
                    ExecInstr(instr);
                }

                Globals = Globals.Append(new GlobalVar {Type = global.Type, Val = PopU64()}).ToArray();
            }
        }

        private void ExecInstr(Instruction instr)
        {
            //fmt.Printf("%s %v\n", instr.GetOpname(), instr.Args)
            var op = Instr.GetInstrFn()[instr.Opcode];
            op(ref this, instr.Args);
        }

        private int StackSize()
        {
            return OperandStack.StackSize();
        }

        /* block stack */
        public void EnterBlock(byte opcode, ref FuncType bt, ref Instruction[] instrs)
        {
            var bp = StackSize() - (bt.ParamTypes?.Length ?? 0);
            var cf = new ControlFrame(opcode, ref bt, ref instrs, bp);
            ControlStack.PushControlFrame(ref cf);
            if (opcode == OpCodes.Call)
            {
                Local0Idx = (uint) bp;
            }
        }

        public void InitFuncs()
        {
            LinkNativeFuncs();
            for (var i = 0; i < Module.FuncSec.Length; i++)
            {
                var ftIdx = Module.FuncSec[i];
                var ft = Module.TypeSec[ftIdx];
                var code = Module.CodeSec[i];
                Funcs = Funcs.Append(VmFunc.NewInternalFunc(ft, code)).ToArray();
            }
        }

        public void InitTable()
        {
            if (Module.TableSec.Length > 0)
            {
                Table = new Table(Module.TableSec[0]);
            }

            if (Module.ElemSec == null)
            {
                return;
            }

            foreach (var elem in Module.ElemSec)
            {
                foreach (var instr in elem.Offset)
                {
                    ExecInstr(instr);
                }

                var offset = PopU32();
                for (var i = 0; i < elem.Init.Length; i++)
                {
                    var funcIdx = elem.Init[i];
                    Table.SetElem(offset + (uint) i, Funcs[funcIdx]);
                }
            }
        }

        private void LinkNativeFuncs()
        {
            foreach (var imp in Module.ImportSec)
            {
                if (imp.Desc.Tag == ConstData.ImportTagFunc && imp.Module == "env")
                {
                    var ft = Module.TypeSec[imp.Desc.FuncType];
                    switch (imp.Name)
                    {
                        case "print_char":
                            Funcs = Funcs.Append(VmFunc.NewExternalFunc(ft, Native.PrintChar)).ToArray();
                            break;
                        case "assert_true":
                            Funcs = Funcs.Append(VmFunc.NewExternalFunc(ft, Native.AssertTrue)).ToArray();
                            break;
                        case "assert_false":
                            Funcs = Funcs.Append(VmFunc.NewExternalFunc(ft, Native.AssertFalse)).ToArray();
                            break;
                        case "assert_eq_i32":
                            Funcs = Funcs.Append(VmFunc.NewExternalFunc(ft, Native.AssertEqI32)).ToArray();
                            break;
                        case "assert_eq_i64":
                            Funcs = Funcs.Append(VmFunc.NewExternalFunc(ft, Native.AssertEqI64)).ToArray();
                            break;
                        case "assert_eq_f32":
                            Funcs = Funcs.Append(VmFunc.NewExternalFunc(ft, Native.AssertEqF32)).ToArray();
                            break;
                        case "assert_eq_f64":
                            Funcs = Funcs.Append(VmFunc.NewExternalFunc(ft, Native.AssertEqF64)).ToArray();
                            break;
                        default:
                            Panic.Run("TODO");
                            break;
                    }
                }
            }
        }

        public void ExitBlock()
        {
            var cf = ControlStack.PopControlFrame();
            ClearBlock(ref cf);
        }

        private void ClearBlock(ref ControlFrame cf)
        {
            var results = PopU64S(cf.Bt.ResultTypes?.Length ?? 0);
            PopU64S(OperandStack.StackSize() - cf.Bp);
            PushU64S(results);
            if (cf.Opcode != OpCodes.Call || ControlStack.ControlDepth() <= 0) return;
            var (lastCallFrame, _) = ControlStack.TopCallFrame();
            if (lastCallFrame?.Bp != null) Local0Idx = (uint) lastCallFrame?.Bp;
        }

        public void ResetBlock(ref ControlFrame cf)
        {
            var results = PopU64S(cf.Bt.ParamTypes?.Length ?? 0);
            PopU64S(OperandStack.StackSize() - cf.Bp);
            PushU64S(results);
        }

        private ulong[] PopU64S(int n)
        {
            return OperandStack.PopU64S(n);
        }

        private void PushU64S(ulong[] vals)
        {
            OperandStack.PushU64S(vals);
        }

        public ulong GetOperand(uint idx)
        {
            return OperandStack.GetOperand(idx);
        }

        public uint PopU32()
        {
            return OperandStack.PopU32();
        }

        public ulong PopU64()
        {
            return OperandStack.PopU64();
        }

        public void PushU64(ulong val)
        {
            OperandStack.PushU64(val);
        }

        public void PushU32(uint val)
        {
            OperandStack.PushU32(val);
        }

        public void PushS32(int val)
        {
            OperandStack.PushS32(val);
        }

        public void PushS64(long val)
        {
            OperandStack.PushS64(val);
        }

        public bool PopBool()
        {
            return OperandStack.PopBool();
        }

        public ControlFrame PopControlFrame()
        {
            return ControlStack.PopControlFrame();
        }
    }
}